/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package hhf.flex.ascreator;

import hhf.flex.annotations.entities.FlexTransient;
import hhf.flex.ascreator.exceptions.ASFileExistException;
import hhf.flex.ascreator.exceptions.IgnoredClassException;
import hhf.flex.ascreator.exceptions.IllegalClassException;
import hhf.flex.ascreator.exceptions.NoAnnotationClassException;
import hhf.flex.ascreator.exceptions.NoCommentClassException;
import hhf.flex.ascreator.exceptions.NoSuperClassException;
import hhf.flex.ascreator.patterns.ASPattern;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 *
 * @author HHFrancois
 */
public abstract class ASCreator {

    protected String path;
    protected Map<String, Object> options;
    protected boolean legacyCollection;
//	private OutputStreamWriter out;

    public ASCreator(String path, Map<String, Object> options) {
        this.path = path;
        if (options != null) {
            this.options = options;
            this.legacyCollection = (Boolean) options.get("legacy-collection");
        }
    }

    private void writeHeader(OutputStreamWriter out) {
        try {
            ASPattern.writeHeader(out, this.getClass().getSimpleName());
        } catch (IOException ex) {
        }
    }

    /**
     * Retourne le package pour as
     * @return
     */
    protected abstract String getASPackage();

    /**
     * test la class pour savoir s'il faut la créer.
     * @return
     */
    protected abstract void checkClass() throws IllegalClassException, IgnoredClassException;

    /**
     * Retourne si la classe est une interface
     * @return
     */
    protected abstract boolean isInterface();

    /**
     * Retourne le nom de la classe as a créer.
     * @return
     */
    protected abstract String getASClassName();

    /**
     * Retourne le commentaire de la classe as a créer.
     * @return
     */
    protected abstract String getASClassComment() throws NoCommentClassException;

    /**
     * Retourne le nom de la superclasse as a créer.
     * @return
     */
    protected abstract String getASSuperClassName() throws NoSuperClassException;

    /**
     * Crée les classes correspondants au classes seeAlso,
     * c'est à dire les classe nécéssaire à la serialisation de la classe.
     * @return
     */
    protected abstract void createSeeAlsoASClass();

    /**
     * Retourne le nom des interfaces as de la classe à créer.
     * @return
     */
    protected abstract Collection<String> getASInterfacesName();

    /**
     * Retourne la liste des classes à rajouter dans les imports
     * @return
     */
    protected abstract Set<String> getASImports();

    /**
     * Retourne la liste des evenement que la classe as doit exposer.
     * Retourne des couples name/type
     * @return
     */
    protected abstract Map<String, String> getASEvents();

    /**
     * Retourne la liste des espaces de nom nécessaire à la classe.
     * ex : mx_internal
     * @return
     */
    protected abstract Set<String> getASNamespaces();

    /**
     * Ecrit le corp du constructeur de la classe.
     * @param out
     */
    protected abstract void writeASBodyConstructor(OutputStreamWriter out) throws IOException;

    /**
     * Ecrit les champs et les methodes de la classe.
     * @param out
     */
    protected abstract void writeASFieldsAndMethods(OutputStreamWriter out) throws IOException;

    /**
     * Ecrit la classe
     */
    public void write() {
        OutputStreamWriter out = null;
        try {
            checkClass();
            String pack = getASPackage();
            String className = getASClassName();
            out = createFile(pack, className);
            writeHeader(out);
            out.write("package " + pack + " {\n");
            out.write("\n");
            insertASImports(out);
            insertASNamespaces(out);
            insertASEvents(out);
            insertClass(out);
            out.write("}");
        } catch (ASFileExistException ex) {
            // la classe existe déja, on ignore
        } catch (IgnoredClassException ex) {
            // la classe n'a pas à etre créer
        } catch (IllegalClassException ex) {
            System.out.println("Error : " + ex.getMessage());
        } catch (IOException ex) {
            System.out.println("Error during creation Service Files : " + ex.getMessage());
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException ex) {
            }
        }
    }

    protected void insertClass(OutputStreamWriter out) throws IOException {
        try { // ecriture du commentaire
            String comment = getASClassComment().replaceAll("\n", "\n\t *");
            out.write("\t/**\n");
            out.write("\t *" + comment + "/\n");
        } catch (NoCommentClassException e) {
        }
        try {
            Collection<String> annos = getASClassAnnotations();
            for (String anno : annos) {
                out.write("\t" + anno + "\n"); // ecriture des annotations sur la classe
            }
        } catch (NoAnnotationClassException nace) {
        }
        out.write("\tpublic " + (isInterface() ? "interface" : "dynamic class") + " " + getASClassName() + " ");
        try {
            out.write("extends " + getASSuperClassName() + " ");
        } catch (NoSuperClassException ex) {
        }
        Collection<String> interfaces = getASInterfacesName();
        if (interfaces != null && interfaces.size() > 0) {
            out.write("implements");
            Iterator<String> ite = interfaces.iterator();
            while (ite.hasNext()) {
                String interf = ite.next();
                out.write(" " + interf);
                if (ite.hasNext()) {
                    out.write(",");
                } else {
                    out.write(" ");
                }
            }
        }
        out.write("{\n");
        out.write("\n");
        insertASConstructor(out);
        writeASFieldsAndMethods(out);
        out.write("\t}\n");
        createSeeAlsoASClass();
    }

    /**
     * Retourne les annotations java au format actionScript pour la classe
     * @param field
     * @param fieldType
     * @return
     */
    protected abstract Collection<String> getASClassAnnotations() throws NoAnnotationClassException;

    /**
     * Constructeur de la classe AS
     * @param out
     * @throws IOException
     */
    protected void insertASConstructor(OutputStreamWriter out) throws IOException {
        if (!isInterface()) {
            out.write("\t\tpublic function " + getASClassName() + "() {\n");
            writeASBodyConstructor(out);
            out.write("\t\t}\n");
            out.write("\n");
        }
    }

    /**
     * Créé le fichier .as representant la classe
     * @param pack
     * @param className
     * @return
     */
    protected OutputStreamWriter createFile(final String pack, final String className) throws ASFileExistException {
        try {
            File rep = new File(path, pack.replaceAll("\\.", "/"));
            if (!rep.exists()) {
                rep.mkdirs();
            }
            File file = new File(rep, className + ".as");
            if (file.exists()) {
                throw new ASFileExistException();
            }
            return new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
        } catch (UnsupportedEncodingException ex) {
        } catch (FileNotFoundException ex) {
        }
        return null;
    }

    /**
     * insert les imports.
     * @throws IOException
     */
    protected void insertASImports(OutputStreamWriter out) throws IOException {
        Set<String> imports = getASImports();
        if (imports != null) {
            for (String imp : imports) {
                out.write("\timport " + imp + ";\n");
            }
            if (imports.size() > 0) {
                out.write("\n");
            }
        }
    }

    /**
     * insert les evenements.
     * @throws IOException
     */
    protected void insertASEvents(OutputStreamWriter out) throws IOException {
        Map<String, String> events = getASEvents();
        if (events != null) {
            for (String name : events.keySet()) {
                String event = events.get(name);
                out.write("\t[Event(name=\"" + name + "\", type=\"" + event + "\")]\n");
            }
            if (events.size() > 0) {
                out.write("\n");
            }
        }
    }

    /**
     * insert les evenements.
     * @throws IOException
     */
    protected void insertASNamespaces(OutputStreamWriter out) throws IOException {
        Set<String> nss = getASNamespaces();
        if (nss != null) {
            for (String ns : nss) {
                out.write("\tuse namespace " + ns + ";\n");
            }
            if (nss.size() > 0) {
                out.write("\n");
            }
        }
    }

    /**
     * Insere les methodes d'acces a un field, avec eventuellement ses annotations
     * @param out
     * @param fieldName
     * @param fieldType
     * @param override
     * @param asAnnotations
     * @throws IOException
     */
    protected void insertASFieldAccessors(OutputStreamWriter out, String fieldName, String fieldType, boolean isInterface, boolean isAbstract, boolean isOverrider, Collection<String> asAnnotations) throws IOException {
        String overrider = "";
        if (asAnnotations != null) {
            for (String asAnnotation : asAnnotations) {
                out.write("\t\t" + asAnnotation + "\n");
            }
        }
        if (isOverrider) {
            overrider = "override ";
        }
        out.write("\t\t");
        if (!isInterface) {
            out.write(overrider + "public ");
        }
        out.write("function get " + fieldName + "():" + fieldType);
        if (!isInterface) {
            out.write(" {\n");
            if (!isAbstract) {
                out.write("\t\t\treturn this._" + fieldName + ";\n");
            } else {
                out.write("\t\t\tthrow new IllegalOperationError(\"Method get " + fieldName + " must be implemented in concret class " + getASClassName() + "\");\n");
            }
            out.write("\t\t}\n");
        } else {
            out.write(";\n");
        }
        out.write("\t\t");
        if (!isInterface) {
            out.write(overrider + "public ");
        }
        out.write("function set " + fieldName + "(" + fieldName + ":" + fieldType + "):void");
        if (!isInterface) {
            out.write(" {\n");
            if (!isAbstract) {
                out.write("\t\t\tthis._" + fieldName + " = " + fieldName + ";\n");
            } else {
                out.write("\t\t\tthrow new IllegalOperationError(\"Method set " + fieldName + " must be implemented in concret class " + getASClassName() + "\");\n");
            }
            out.write("\t\t}\n\n");
        } else {
            out.write(";\n\n");
        }
    }

    /**
     * Insere le getter children, permettant les arborescence d'objet non similaires
     * Generé par la presence de l'annoation FlexChildren
     * @param out
     * @param fieldName
     * @param fieldType
     * @throws IOException
     */
    protected void insertASChildreField(OutputStreamWriter out, String fieldName, String fieldType) throws IOException {
        out.write("\t\t[Transient]");
        out.write("\t\tpublic function get children():" + fieldType + " {\n");
        out.write("\t\t\treturn this." + fieldName + ";\n");
        out.write("\t\t}\n");

    }

    /**
     * Insere le field et les methodes d'acces, avec eventuellement ses annotations
     * @param out
     * @param fieldName
     * @param fieldType
     * @param override
     * @param asAnnotations
     * @throws IOException
     */
    protected void insertASField(OutputStreamWriter out, String fieldName, String fieldType, boolean isInterface, boolean isAbstract, boolean isOverrider, Collection<String> asAnnotations) throws IOException {
        if (!isInterface) {
            out.write("\t\tprivate var _" + fieldName + ":" + fieldType + ";\n");
        }
        insertASFieldAccessors(out, fieldName, fieldType, isInterface, isAbstract, isOverrider, asAnnotations);
    }

    /**
     * Enrichit la listes des imports nécéssaire à la classe.
     * Lors de la découverte des classes, si celle ci n'existe pas, elle est créé.
     * @param method
     * @param imports
     */
    protected void getASImportsFromMethod(final Method method, Set<String> imports) {
        if (method.isAnnotationPresent(FlexTransient.class) || Modifier.isStatic(method.getModifiers())) {
            return;
        }
        Type returnType = method.getGenericReturnType();
        discoverASClassFromType(returnType, imports, true);
        for (Type type : method.getGenericParameterTypes()) {
            discoverASClassFromType(type, imports, true);
        }
    }

    /**
     * Enrichit la listes des imports nécéssaire à la classe.
     * Lors de la découverte des classes, si celle ci n'existe pas, elle est créé.
     * @param class
     * @param imports
     */
    protected void getASImportsFromClass(final Class clazz, Set<String> imports) {
        discoverASClassFromType(clazz.getGenericSuperclass(), imports, true);
        for (Type type : clazz.getGenericInterfaces()) {
            discoverASClassFromType(type, imports, true);
        }
    }

    /**
     * Decouvre les types generiques, les stoke dans imports.
     * Genere les classes nécéssaires.
     * @param type : type à découvrir
     * @param imports : liste contenant la liste de toutes les classes découvertes
     * @param ignoreASType : définit si la liste contient la liste des type connus par AS
     */
    private void discoverASClassFromType(Type type, Set<String> imports, boolean ignoreASType) {
        ASCreator.discoverASClassFromType(path, type, imports, options, ignoreASType, legacyCollection);
    }

    public static void discoverASClassFromType(String path, Type type, Map<String, Object> options) {
        ASCreator.discoverASClassFromType(path, type, new HashSet<String>(), options, true, true);
    }

    /**
     * Decouvre les types generiques, les stoke dans imports.
     * Genere les classes nécéssaires.
     * @param type : type à découvrir
     * @param imports : liste contenant la liste de toutes les classes découvertes
     * @param ignoreASType : définit si la liste contient la liste des type connus par AS
     */
    private static void discoverASClassFromType(String path, Type type, Set<String> imports, Map<String, Object> options, boolean ignoreASType, boolean legacyCollection) {
        Class clazz;
        if (type == null) {
        } else if (type instanceof WildcardType) { // Collection<? extends MaClasse>
            WildcardType wildcardType = (WildcardType) type;
            Type[] upperBounds = wildcardType.getUpperBounds();
            for (Type t : upperBounds) {
                discoverASClassFromType(path, t, imports, options, ignoreASType, legacyCollection);
            }
        } else if (type instanceof ParameterizedType) { // Collection<MaClasse>, Map<MaCls1, MaCls2>
            ParameterizedType paramType = (ParameterizedType) type;
            discoverASClassFromType(path, paramType.getRawType(), imports, options, ignoreASType, legacyCollection);
            for (Type stype : paramType.getActualTypeArguments()) {
                discoverASClassFromType(path, stype, imports, options, ignoreASType, legacyCollection);
            }
        } else if (type instanceof GenericArrayType) { // MaClasse[]
            GenericArrayType genericArrayType = (GenericArrayType) type;
            Type genericCompType = genericArrayType.getGenericComponentType();
            discoverASClassFromType(path, genericCompType, imports, options, ignoreASType, legacyCollection);
        } else if (type instanceof Class) { // MaClasse
            clazz = (Class) type;
            if (clazz.equals(Serializable.class) || clazz.equals(Object.class) || clazz.equals(Class.class)) {
            } else if (clazz.isEnum()) {
                ASEnumCreator asClassCreator = new ASEnumCreator(path, clazz, options);
                asClassCreator.write();
                if (!ignoreASType) {
                    imports.add("String"); // une enumeration est vue comme une string pour AS
                }
            } else {
                String asClass = ASCreatorTools.getASClassName(clazz, legacyCollection);
                if (asClass == null) { // le type n'existe pas en AS il faut peut être l'importer et le créer.
                    if (clazz != void.class) {
                        ASClassCreator asClassCreator = new ASClassCreator(path, clazz, options);
                        asClassCreator.write();
                        imports.add(clazz.getName());
                    }
                } else if (!ignoreASType) {
                    imports.add(asClass);
                }
            }
        } else {
            System.out.println("discoverASClassFromType type unknown : " + type.getClass().getName());
        }
    }

    /**
     * Retourne le type d'element que retourne une classe Collection ou Map
     * Tiens compte des Map
     * @param type
     * @return
     */
    public String getASArrayElementType(Type type) {
        String result = null;
        discoverASClassFromType(type, new HashSet<String>(), false); // on decouvres toute les classes betement...

        // on essaye de faire mieux
        Set<String> returnTypes = new HashSet<String>();
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            Type rawType = parameterizedType.getRawType();
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            if (rawType instanceof Class) {
                Class cls = (Class) rawType;
                if (Map.class.isAssignableFrom(cls)) { // c'est une map, seul le deuxieme argument est pertinent
                    if (actualTypeArguments.length == 2) {
                        Type t = actualTypeArguments[1];
                        discoverASClassFromType(t, returnTypes, false);
                    } else {
                        System.out.println("MAP avec plus ou moins de deux parametres... Map<A, B, C> ???? nb : " + actualTypeArguments.length);
                    }
                } else if (Collection.class.isAssignableFrom(cls)) { // c'est une collection, seul le premier argument est pertinent (il n'y en a qu'un)
                    if (actualTypeArguments.length == 1) {
                        Type t = actualTypeArguments[0];
                        discoverASClassFromType(t, returnTypes, false);
                    } else {
                        System.out.println("COLLECTION avec plus ou moins d'un parametre ... Collection<A, B> ???? nb : " + actualTypeArguments.length);
                    }
                }
            }
        }
        for (String arrayType : returnTypes) {
            result = arrayType;
        }
        return result;
    }

    public static Field getField(Class cls, String fieldName) throws NoSuchFieldException {
        if (cls.equals(Object.class)) {
            throw new NoSuchFieldException();
        }
        try {

            return cls.getDeclaredField(fieldName);
        } catch (Exception ex) {
            return getField(cls.getSuperclass(), fieldName);
        }
    }

    public static Method getMethod(Class cls, String methodName, Class... parametersTypes) throws NoSuchMethodException {
        if (cls == null) {
            throw new NoSuchMethodException();
        }
        if (cls.equals(Object.class)) {
            throw new NoSuchMethodException();
        }
        try {
            return cls.getDeclaredMethod(methodName, parametersTypes);
        } catch (Exception ex) {
            return getMethod(cls.getSuperclass(), methodName, parametersTypes);
        }

    }
}
